iT邦幫忙

2022 iThome 鐵人賽

DAY 12
2
Modern Web

真的好想離開 Vue 3 新手村 feat. CompositionAPI系列 第 12

真的好想離開 Vue 3 新手村 - Day 12: 認識 nextTick 與 DOM 響應更新時機 feat. template ref

  • 分享至 

  • xImage
  •  

前言

Vue 在官方文件 - Reactivity Fundamentals 中,正式進入 reactive()ref() 之前,先提到了 nextTick() 這個 API。

第一次看的時候真的是一頭霧水。

學習 Vue 一段時間後回頭重看,終於知道這是在幹嘛,也發現自己當初缺失的那塊拼圖;所以這篇會先以新手的視角切入,整理在新手學習 nextTick() 之前,應該要了解的事情,再來才會去了解 nextTick()

誰適合看這篇

  • 不懂為什麼需要 nextTick() 和生命週期鉤子
  • 不知道 nextTick() 的用途

Outline

  • 你需要知道的 Vue
  • Vue 什麼時候更新 DOM
  • nextTick()
    • 使用範例
    • 取 DOM:搭配 template ref

你需要知道的 Vue

  1. Vue 可以做到由資料驅動畫面,是因為他在背後幫開發者做了大量的工作,事先將 DOM 和資料做連結,所以我們才可以用簡潔的模板語法,將資料渲染到 DOM 上,也就是 Vue 所謂的 Declarative Rendering (宣告式渲染)。
<div>
    <h2>今日訊息:{{ message }}</h2>
</div>
const message = ref("第 12 天,還行嗎?");
  1. 搭配響應式系統,Vue 偵測到資料改動後, 除了更新相依資料,還會幫忙比對 Virtual DOM,並重新渲染更新的區塊。
    以下面的程式碼為例,我們只要修改 message 這筆資料,Vue 就會幫我們重新渲染:
<div>
    <h2>今日訊息:{{ message }}</h2>
</div>
const message = ref("第 12 天,還行嗎?");
setTimeout(() => {
  message.value = "睏到不行";
}, 1000);

我們修改資料的步驟,就是流程圖中的紅色區塊(component reactive state/元件的響應狀態),剩下的事情由 Vue 處理。

所以,基本上,開發者在 Vue 框架下開發,可以專注處理資料的變化和邏輯,不需要自己操作 DOM,也就不太會用到一堆 document.querySelector 等。

那 DOM 的事情都改由 Vue 處理了,那如果在 DOM 變化的過程前後 (這裡泛指建立、更新、銷毀),我們想要做事怎麼辦

這就是為什麼會提供 nextTick() 和生命週期鉤子(Hook)。

今天只會講到 nextTick(),而他被呼叫的時間就在 Vue 幫我們更新 DOM 的時間點後,所以我們要先認識 Vue 什麼時候更新 DOM。

Vue 什麼時候更新 DOM

響應式資料更新後,Vue 會先同步更新相依數據,再以非同步的方式去更新 DOM。

這樣的好處是可以節省效能,如果開發者短時間內修改了好幾次的資料,其實 Vue 只需要渲染最終的結果,就能省去中間一直重新渲染 DOM 的效能。

数据的修改,同时会触发dom渲染,即执行一个同步修改数据任务,再执行一个dom渲染的异步任务,此时完成了一轮loop,如果此时去获取dom,那必然会在异步任务执行前获取,当然获取不到,想要获取更新后的dom,必须再一次开启一个时间循环,即使用nexttick。

所以說,更新渲染 DOM 這件事是非同步的,等於:不知道 DOM 什麼時候會完成更新
那如果你修改完資料之後,要拿新的 DOM 元素內狀態 (如:寬、高) 來做事怎麼辦?

所以 Vue 提供了 nextTick 這個 API,他被呼叫的時機,會是在 DOM 更新渲染完成後。

nextTick

  • 呼叫時機:數據更新後,DOM 非同步更新也完成後
  • 會回傳 promise
    • 可以把 callback 傳進去
    • 也可以在 async function 裡面 await nextTick() ,等 nextTick() 被呼叫後,再接著處理
  • 可以拿到資料更新後,完成更新的 DOM 狀態(如:寬、高)來做邏輯處理

欸所以那個 tick 到底是什麼?

其實就是所謂的「事件循環」

每次修改資料後,Vue 要幫忙做一堆工作,至少包括:

  1. (同步) 修改資料
  2. (非同步) 修改 DOM >>> 等這個 tick 跑完,再執行 nextTick
  3. (非同步) 執行 nextTick callback

使用範例

的確是蠻少用到的,除非需要拿到資料更新後的 DOM 狀態來做事,我一時之間也想不到什麼情境,在重新認識 Vue.js 裡有提到滾動的例子

有一個有高度限制的 <div> 在裡面用 v-for 渲染 messages 清單,每次 <input> 內容送出後,會將輸入內容 push 到 messages 陣列中,內部會新增一個新的 message 清單項目 。
我們想要在每次新增後,都將 <div> 捲到最底部,才可以看到最新的輸入內容。
期望效果:

問題範例

<div class="messages" ref="messagesDiv">
  <div v-for="message in messages" :key="message">{{ message }}</div>
</div>
<input
  type="text"
  v-model.trim="newMessage"
  @keydown.enter="addToMessages"
/>
function addToMessages() {
  messages.value.push(newMessage.value);

  const messagesDiv = document.querySelector(".messages");
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

上面這段程式碼的呈現結果:

仔細觀察程式碼的畫面,會發現每次「捲到底部」都是停在上一次新增的內容地方,因為在這個事件迴圈中,拿到的 messagesDiv.scrollHeight 還是前一次的高度。

解決方法

  1. 將 callback 函式直接寫在 nextTick()
function addToMessages() {
  messages.value.push(newMessage.value);

  nextTick(() => {
    const messagesDiv = document.querySelector(".messages");
    messagesDiv.scrollTop = messagesDiv.scrollHeight;
  });
}
  1. 將 callback 函式直接寫在 await nextTick() 後面,等到接到 nextTick 完成再繼續執行
async function addToMessages() {
    messages.value.push(newMessage.value);

    await nextTick();
    const messagesDiv = document.querySelector(".messages");
    messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

使用到 nextTick 通常是為了取 DOM 元素,那在 Vue 3 要怎麼拿 DOM 元素比較好?

取 DOM:搭配 template ref

Vue 在 <tamplate> 提供了特殊的 ref 屬性,透過 ref 可以直接拿到渲染後的 DOM 元素的參照(reference)。

只要先在 <script> 中宣告和 <template> 中 ref 屬性同名的變數,就可以透過這個變數拿到 DOM。

import { ref } from 'vue'
const messages = ref(['牛肉湯', '肉燥飯', '鍋燒意麵', '白糖粿'])
const newMessage = ref('')

const messagesDiv = ref(null)

async function addToMessages() {
  messages.value.push(newMessage.value)

  await nextTick();

  //在這裡取到的 messagesDiv 已經變成 DOM 元素了
  messagesDiv.value.scrollTop = messagesDiv.value.scrollHeight
}

注意事項
注意要在元素渲染、掛載到 DOM 上之後,才能透過 ref 屬性取到該元件;所以一開始的變數(messagesDiv)才會綁定 null,因為 Vue 在讀取這段程式碼時,還在準備渲染,這時候還選不到 messagesDiv 這個元素。

結尾

平常很少需要把更新後的 DOM 狀態拿出來做邏輯處理,比較常見的情況,應該是瀑布流或是輪播圖,要在 API 拿資料回來後,根據新的 DOM 去計算位置等等。
但因為時間太趕,沒有找到相對應的範例,如果之後有踩到坑,會再補充。

今天的內容有點雜,整理重點如下:

  1. Vue 是資料驅動,通常都由 Vue 幫我們處理跟 DOM 相關的事情,如:掛載、更新,想在這些階段處理事情,就需要生命週期鉤子或 nextTick()
  2. 響應式資料更新後,Vue 會先同步更新相依數據,再以非同步的方式去更新 DOM。
  3. nextTick() 會在 DOM 非同步更新完成後被呼叫,在這個呼叫時機點,可以拿資料更新後的 DOM 狀態來做事
  4. 取得 DOM 元素可以透過 Vue 提供的 ref 屬性,注意要在元素掛載上去後才能拿到。

參考資料


上一篇
真的好想離開 Vue 3 新手村 - Day 11: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (下)
下一篇
真的好想離開 Vue 3 新手村 - Day 13: v-on 語法、修飾符與找不到的 console.log
系列文
真的好想離開 Vue 3 新手村 feat. CompositionAPI31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言